//
//  PSDatabase.m
//  PSExtensions
//
//  Created by PoiSon on 15/9/28.
//  Copyright © 2015年 yerl. All rights reserved.
//

#import "PSDicRecord_private.h"

@interface PSModel()
@property (nonatomic, strong) id<PSSetProtocol> modifyFlag;
@property (nonatomic, strong, readonly) PSTable *table;
@property (nonatomic, strong, readonly) PSDbConfig *config;
@end

@implementation PSModel{
    NSString *_configName;
    PSTable *_table;
}
@dynamic ID;

+ (instancetype)dao{
    //cache daos
    static NSMutableDictionary<NSString *, PSModel *> *daoMap;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        daoMap = [NSMutableDictionary new];
    });
    @synchronized(self) {
        return [daoMap ps_objectForKey:NSStringFromClass([self class]) setDefault:^id{
            return [[self alloc] init];
        }];
    }
}

+ (instancetype)modelWithAttributes:(NSDictionary<NSString *, id> *)attrs{
    return [[self alloc] initWithAttributes:attrs];
}

- (NSString *)configName{
    return self.config.name;
}

- (instancetype)use:(NSString *)configName{
    _configName = [configName copy];
    return self;
}

- (PSDbConfig *)config{
    PSDbConfig *config;
    if (_configName) {
        config = [PSDbKit configForName:_configName];
        PSAssert(config, @"can not find config named: %@", _configName);
    }else{
        config = [PSDbKit configForModel:self.class];
    }
    if (!config) {
        config = [PSDbKit brokenConfig];
    }
    return config;
}

- (PSTable *)table{
    returnValIf(_table, _table);
    _table = [PSDbKit tableForModel:self.class];
    
    static PSTable *_default = nil;
    return _table ?: _default ?: (_default = [PSTable buildTableWithModel:self.class]);
}

#pragma mark - Getter/Setter
- (id<PSSetProtocol>)modifyFlag{
    return _modifyFlag ?: ({_modifyFlag = [self.config.containerFactory createSet];});
}

- (void)setValue:(id)value forKey:(NSString *)aKey{
    if (!value) {
        [self removeValueForKey:aKey];
        return;
    }
    [self.modifyFlag addValue:aKey];
    [super setValue:value forKey:aKey];
}

- (void)setDictionary:(NSDictionary<NSString *,id> *)aDictionary{
    [super setDictionary:aDictionary];
}

- (void)putValue:(id)anObject forKey:(NSString *)aKey{
    [super setValue:anObject forKey:aKey];
}

- (void)putDictionary:(NSDictionary<NSString *, id> *)aDictionary{
    [super setDictionary:aDictionary];
}

- (void)removeValueForKey:(NSString *)aKey{
    [self.modifyFlag removeValue:aKey];
    [super removeValueForKey:aKey];
}

- (void)removeAllValues{
    [self.modifyFlag removeAllValues];
    [super removeAllValues];
}

#pragma mark - dynamic property getter/setter
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSString *selector = NSStringFromSelector(aSelector);
    BOOL isSetter = [selector hasPrefix:@"set"];
    NSString *property = [[selector ps_removePrefix:@"set"] ps_removeSuffix:@":"];
    PSColumn *column = [self.table columnForName:property];
    
    if (column) {
        return isSetter ? column.convertor.setterSignature : column.convertor.getterSignature;
    }else{
        return [super methodSignatureForSelector:aSelector];
    }
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    return self;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSString *selector = NSStringFromSelector(anInvocation.selector);
    BOOL isSetter = [selector hasPrefix:@"set"];
    NSString *property = [[selector ps_removePrefix:@"set"] ps_removeSuffix:@":"];
    PSColumn *column = [self.table columnForName:property];
    
    if (column) {
        if (isSetter) {
            void *buffer = calloc(1, anInvocation.methodSignature.frameLength);
            [anInvocation getArgument:buffer atIndex:2];
            
            id value = [column.convertor objectForBuffer:buffer];
            [self setValue:value forKey:column.name];
            free(buffer);
        }else{
            void *buffer = calloc(1, anInvocation.methodSignature.methodReturnLength);
            
            id value = [self valueForKey:column.name];
            // property value revise
            id newValue = [column.convertor getBuffer:buffer fromObject:value];
            doIf(newValue, [self putValue:newValue forKey:column.name]);
            
            [anInvocation setReturnValue:buffer];
            doIf(![anInvocation argumentsRetained], [anInvocation retainArguments]);
            free(buffer);
        }
    }else{
        [super forwardInvocation:anInvocation];
    }
}

- (id)objectForKeyedSubscript:(NSString *)key{
    return [self valueForKey:key];
}
@end

#pragma mark - implementation of Operation
@implementation PSModel(Operation)
- (BOOL)save{
    PSDbConnection *conn = [self.config getOpenedConnection];
    @try {
        PSDbStatement *statment = [conn prepareStatement:[PSSqlBuilder forSave:self.table attrs:self->_attrs]];
        BOOL result = [statment executeUpdate];
        if (!result) {
            PSLog(conn.lastErrorMessage);
        }else{
            [self putValue:@([statment generatedKey]) forKey:self.table.key];
        }
        return result;
    }
    @finally {
        [self.config close:conn];
    }
}

- (BOOL)update{
    returnValIf(self.modifyFlag.count < 1, YES);
    returnValIf(self.modifyFlag.count == 1 && [self.modifyFlag contains:@"ID"], YES);
    
    id idValue = [self valueForKey:self.table.key];
    PSAssert(idValue && [idValue integerValue] > 0, @"You can't update model without Primary Key");
    
    PSSql *updateSql = [PSSqlBuilder forUpdate:self.table attrs:self->_attrs modifyFlag:[self.modifyFlag toSet]];
    return [self updateWithSql:updateSql];
}

- (BOOL)updateAll{
    [self.modifyFlag addValuesFormArray:[self allKeys]];
    return [self update];
}

- (BOOL)saveOrUpdate{
    PSSql *sql = [PSSqlBuilder forReplace:self.table attrs:self->_attrs];
    return [self updateWithSql:sql];
}

- (BOOL)delete{
    id idValue = [self valueForKey:self.table.key];
    PSAssert(idValue, @"You can't delete model without primary key value");
    return [self deleteById:self.ID];
}

- (BOOL)deleteById:(NSInteger)idValue{
    PSAssertParameter(idValue > 0);
    PSSql *deleteSql = [PSSqlBuilder forDelete:self.table byCondition:@"ID = ?" withArgs:@[@(idValue)]];
    return [self updateWithSql:deleteSql];
}

- (BOOL)deleteByCondition:(NSString *)condition, ...{
    PSAssertParameter(condition != nil && condition.length > 0);
    PSSql *deleteSql = [PSSqlBuilder forDelete:self.table byCondition:condition withArgs:va_array(condition)];
    return [self updateWithSql:deleteSql];
}

- (BOOL)deleteAll{
    return [self updateWithSql:[PSSqlBuilder forDelete:self.table byCondition:@"1 = 1" withArgs:nil]];
}

- (NSInteger)count{
    return [self countByCondition:@"1 = 1"];
}

- (NSInteger)countByCondition:(NSString *)condition, ...{
    NSNumber *num = [self queryOneWithSql:[PSSqlBuilder forRowCount:self.table byCondition:condition withArgs:va_array(condition)]];
    return [num integerValue];
}

#pragma mark - Update Models
- (BOOL)update:(NSString *)sql, ...{
    return [self updateWithSql:[PSSql buildSql:sql withArgs:va_array(sql)]];
}

- (BOOL)updateWithSql:(PSSql *)sql{
    PSDbConnection *conn = [self.config getOpenedConnection];
    @try {
        return [[conn prepareStatement:sql] executeUpdate];
    }
    @finally {
        [self.config close:conn];
    }
}

#pragma mark - Find Models
- (NSArray *)findWithSql:(PSSql *)sql{
    PSDbConnection *conn = [self.config getOpenedConnection];
    @try {
        PSQueryResultSet *resultSet = [[conn prepareStatement:sql] executeQuery];
        NSMutableArray<PSModel *> *results = [NSMutableArray new];
        for (NSDictionary<NSString *, id> *attrs in resultSet) {
            id item = [[self.class alloc] initWithAttributes:attrs];
            [results addObject:item];
        }
        return results;
    }
    @finally {
        [self.config close:conn];
    }
}

- (NSArray *)find:(NSString *)sql, ...{
    return [self findWithSql:[PSSql buildSql:sql withArgs:va_array(sql)]];
}

- (NSArray *)findByCondition:(NSString *)condition, ...{
    PSSql *sql = [PSSqlBuilder forFind:self.table columns:@"*" byCondition:condition withArgs:va_array(condition)];
    return [self findWithSql:sql];
}

- (id)findFirstByCondition:(NSString *)condition, ...{
    PSSql *sql = [PSSqlBuilder forFind:self.table columns:@"*" byCondition:condition withArgs:va_array(condition)];
    NSArray *result = [self findWithSql:sql];
    returnValIf(result.count, result[0]);
    return nil;
}

- (NSArray *)findAll{
    PSSql *sql = [PSSqlBuilder forFind:self.table columns:nil byCondition:nil withArgs:nil];
    return [self findWithSql:sql];
}

- (id)findById:(NSInteger)idValue{
    PSAssertParameter(idValue > 0);
    PSSql *sql = [PSSqlBuilder forFind:self.table columns:@"*" byCondition:@"ID = ?" withArgs:@[@(idValue)]];
    return [self findFirstWithSql:sql];
}

- (id)findById:(NSInteger)idValue loadColumns:(NSString *)columns{
    PSAssertParameter(idValue > 0);
    PSSql *sql = [PSSqlBuilder forFind:self.table columns:columns byCondition:@"ID = ?" withArgs:@[@(idValue)]];
    return [self findFirstWithSql:sql];
}

- (id)findFirst:(NSString *)sql, ...{
    NSArray *result = [self findWithSql:[PSSql buildSql:sql withArgs:va_array(sql)]];
    returnValIf([result count], result[0]);
    return nil;
}

- (id)findFirstWithSql:(PSSql *)sql{
    NSArray *result = [self findWithSql:sql];
    returnValIf(result.count, result[0]);
    return nil;
}

- (id)queryOneWithSql:(PSSql *)sql{
    PSDbConnection *conn = [self.config getOpenedConnection];
    @try {
        PSQueryResultSet *result = [[conn prepareStatement:sql] executeQuery];
        returnValIf(result.count < 1, nil);
        
        PSQueryResult *item = [result objectAtIndex:0];
        NSArray *keys = [item allKeys];
        PSAssert(keys.count > 0, @"No columns was queried.");
        PSAssert(keys.count < 2, @"Only ONE column can be queried.");
        return [item objectForKey:[keys objectAtIndex:0]];
    }
    @finally {
        [self.config close:conn];
    }
}

- (id)queryOne:(NSString *)sql, ...{
    return [self queryOneWithSql:[PSSql buildSql:sql withArgs:va_array(sql)]];
}


- (PSPage *)paginate:(NSInteger)pageIndex size:(NSInteger)pageSize withSelect:(NSString *)select where:(NSString *)where, ...{
    PSAssertParameter(pageIndex > 0 && pageSize > 0);
    NSInteger total;
    {
        total = [[self queryOneWithSql:[PSSql buildSql:[@"select count(1) " stringByAppendingString:where] withArgs:va_array(where)]] integerValue];
        returnValIf(total < 1, [PSPage pageWithArray:[NSArray new] index:0 size:0 total:0]);
    }
    
    PSSql *sql = [PSSqlBuilder forPaginateIndex:pageIndex size:pageSize withSelect:select where:where args:va_array(where)];
    NSArray<PSModel *> *result = [self findWithSql:sql];
    return [PSPage pageWithArray:result index:pageIndex size:pageSize total:total];
}
@end

#pragma mark - implementation of Configuration
@implementation PSModel(Configuration)
+ (NSString *)tableName{
    return NSStringFromClass([self class]);
}

+ (NSInteger)version{
    return 1;
}

+ (NSString *)propertyForColumn:(NSString *)column{
    return nil;
}

+ (NSArray<PSSql *> *)migrateForm:(NSInteger)oldVersion to:(NSInteger)newVersion{
    return nil;
}
@end
